{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "ebd66700",
   "metadata": {},
   "source": [
    "## Demo_ACT\n",
    "This is a demo for visualizing the dense plot of hessian matrix for a batch of data.\n",
    "\n",
    "To run this demo from scratch, you need first generate a BadNet attack result by using the following cell"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "b950f4fc",
   "metadata": {},
   "outputs": [],
   "source": [
    "! python ../../attack/badnet.py --save_folder_name badnet_demo"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8f81f973",
   "metadata": {},
   "source": [
    "or run the following command in your terminal\n",
    "\n",
    "```python attack/badnet.py --save_folder_name badnet_demo```"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "87bd9f5a",
   "metadata": {},
   "source": [
    "### Step 1: Import modules and set arguments"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "71b7087b",
   "metadata": {},
   "outputs": [],
   "source": [
    "import sys, os\n",
    "import yaml\n",
    "import torch\n",
    "import numpy as np\n",
    "import torchvision.transforms as transforms\n",
    "\n",
    "sys.path.append(\"../\")\n",
    "sys.path.append(\"../../\")\n",
    "sys.path.append(os.getcwd())\n",
    "from visual_utils import *\n",
    "from utils.aggregate_block.dataset_and_transform_generate import (\n",
    "    get_transform,\n",
    "    get_dataset_denormalization,\n",
    ")\n",
    "from utils.aggregate_block.fix_random import fix_random\n",
    "from utils.aggregate_block.model_trainer_generate import generate_cls_model\n",
    "from utils.save_load_attack import load_attack_result\n",
    "from utils.defense_utils.dbd.model.utils import (\n",
    "    get_network_dbd,\n",
    "    load_state,\n",
    "    get_criterion,\n",
    "    get_optimizer,\n",
    "    get_scheduler,\n",
    ")\n",
    "from utils.defense_utils.dbd.model.model import SelfModel, LinearModel\n",
    "from pyhessian import hessian # Hessian computation"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "2fb719c7",
   "metadata": {},
   "outputs": [],
   "source": [
    "### Basic setting: args\n",
    "args = get_args(True)\n",
    "\n",
    "########## For Demo Only ##########\n",
    "args.yaml_path = \"../../\"+args.yaml_path\n",
    "args.result_file_attack = \"badnet_demo\"\n",
    "######## End For Demo Only ##########\n",
    "\n",
    "with open(args.yaml_path, \"r\") as stream:\n",
    "    config = yaml.safe_load(stream)\n",
    "config.update({k: v for k, v in args.__dict__.items() if v is not None})\n",
    "args.__dict__ = config\n",
    "args = preprocess_args(args)\n",
    "fix_random(int(args.random_seed))\n",
    "\n",
    "save_path_attack = \"../..//record/\" + args.result_file_attack\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "f959b510",
   "metadata": {},
   "source": [
    "### Step 2: Load data"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "b8b67ac9",
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "WARNING:root:save_path MUST have 'record' in its abspath, and data_path in attack result MUST have 'data' in its path\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Files already downloaded and verified\n",
      "Files already downloaded and verified\n",
      "loading...\n",
      "Create visualization dataset with \n",
      " \t Dataset: bd_train \n",
      " \t Number of samples: 50000  \n",
      " \t Selected classes: [0 1 2 3 4 5 6 7 8 9]\n"
     ]
    }
   ],
   "source": [
    "# Load result\n",
    "result_attack = load_attack_result(save_path_attack + \"/attack_result.pt\")\n",
    "selected_classes = np.arange(args.num_classes)\n",
    "\n",
    "# Select classes to visualize\n",
    "if args.num_classes > args.c_sub:\n",
    "    selected_classes = np.delete(selected_classes, args.target_class)\n",
    "    selected_classes = np.random.choice(\n",
    "        selected_classes, args.c_sub-1, replace=False)\n",
    "    selected_classes = np.append(selected_classes, args.target_class)\n",
    "\n",
    "# keep the same transforms for train and test dataset for better visualization\n",
    "result_attack[\"clean_train\"].wrap_img_transform = result_attack[\"clean_test\"].wrap_img_transform \n",
    "result_attack[\"bd_train\"].wrap_img_transform = result_attack[\"bd_test\"].wrap_img_transform \n",
    "\n",
    "# Create dataset\n",
    "if args.visual_dataset == 'clean_train':\n",
    "    visual_dataset = result_attack[\"clean_train\"]\n",
    "elif args.visual_dataset == 'clean_test':\n",
    "    visual_dataset = result_attack[\"clean_test\"]\n",
    "elif args.visual_dataset == 'bd_train':\n",
    "    visual_dataset = result_attack[\"bd_train\"]\n",
    "elif args.visual_dataset == 'bd_test':\n",
    "    visual_dataset = result_attack[\"bd_test\"]\n",
    "else:\n",
    "    assert False, \"Illegal vis_class\"\n",
    "\n",
    "print(\n",
    "    f'Create visualization dataset with \\n \\t Dataset: {args.visual_dataset} \\n \\t Number of samples: {len(visual_dataset)}  \\n \\t Selected classes: {selected_classes}')\n",
    "\n",
    "# Create data loader\n",
    "data_loader = torch.utils.data.DataLoader(\n",
    "    visual_dataset, batch_size=args.batch_size, num_workers=args.num_workers, shuffle=False\n",
    ")\n",
    "\n",
    "# Create denormalization function\n",
    "for trans_t in data_loader.dataset.wrap_img_transform.transforms:\n",
    "    if isinstance(trans_t, transforms.Normalize):\n",
    "        denormalizer = get_dataset_denormalization(trans_t)\n",
    "\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "e3f652e5",
   "metadata": {},
   "source": [
    "### Step 3: Load Model"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "ff67e7b8",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Load model preactresnet18 from badnet_demo\n"
     ]
    }
   ],
   "source": [
    "# Load model\n",
    "model_visual = generate_cls_model(args.model, args.num_classes)\n",
    "model_visual.load_state_dict(result_attack[\"model\"])\n",
    "model_visual.to(args.device)\n",
    "# !!! Important to set eval mode !!!\n",
    "model_visual.eval()\n",
    "print(f\"Load model {args.model} from {args.result_file_attack}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cc952077",
   "metadata": {},
   "source": [
    "### Step 4: Plot Eigenvalues of Hessian\n",
    "\n",
    "Adapted from https://github.com/amirgholami/PyHessian/blob/master/Hessian_Tutorial.ipynb"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "ca1cb3c1",
   "metadata": {},
   "outputs": [],
   "source": [
    "def old_torcheig(A, eigenvectors):\n",
    "    '''A temporary function as an alternative for torch.eig (torch<=1.9)'''\n",
    "    vals, vecs = torch.linalg.eig(A)\n",
    "    if torch.is_complex(vals) or torch.is_complex(vecs):\n",
    "        print('Warning: Complex values founded in Eigenvalues/Eigenvectors. This is impossible for real symmetric matrix like Hessian. \\n We only keep the real part.')\n",
    "\n",
    "    vals = torch.real(vals)\n",
    "    vecs = torch.real(vecs)\n",
    "\n",
    "    # vals is a nx2 matrix. see https://virtualgroup.cn/pytorch.org/docs/stable/generated/torch.eig.html\n",
    "    vals = vals.view(-1,1)+torch.zeros(vals.size()[0],2).to(vals.device)\n",
    "    if eigenvectors:\n",
    "        return vals, vecs\n",
    "    else:\n",
    "        return vals, torch.tensor([])\n",
    "    \n",
    "torch.eig = old_torcheig"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "id": "94612903",
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "/usr/local/anaconda3/lib/python3.9/site-packages/torch/autograd/__init__.py:197: UserWarning: Using backward() with create_graph=True will create a reference cycle between the parameter and its gradient which can cause a memory leak. We recommend using autograd.grad when creating the graph to avoid this. If you have to use this function, make sure to reset the .grad fields of your parameters to None after use to break the cycle and avoid the leak. (Triggered internally at ../torch/csrc/autograd/engine.cpp:1059.)\n",
      "  Variable._execution_engine.run_backward(  # Calls into the C++ engine to run the backward pass\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "The top two eigenvalues of this model are: 36.4870 71.8704\n",
      "Warning: Complex values founded in Eigenvalues/Eigenvectors. This is impossible for real symmetric matrix like Hessian. \n",
      " We only keep the real part.\n"
     ]
    },
    {
     "data": {
      "image/png": "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yxyrkpKSkrRt2zZvTh/wnHqp6LICAMA0vApWp06dUsOGDcs8pkGDBsrIyPCqqEDHvQIBADAnr4JVTEyMdu3aVeYxu3btUnR0tFdFoRgdVgAAmIdXwap79+5avHixfvrpJ7f7f/zxR33++edON2mG55wmr9NnBQCAaXgVrJ544glJUteuXTVlyhRt3LhRv/76qzZu3KjJkyerW7duCgoKUnJysk+LDRzFYYoFQgEAMA+vFght27at5s6dqxEjRmjy5MmaPHmyfZ9hGKpbt64++OADtW3b1meFBhKWWwAAwJy8ClaSdMcdd6hbt26aNWuWtmzZoqysLNWvX1+dOnXSiBEjFBMT48s6AwoLhAIAYE5eBytJatiwoR5//HFf1eL38vPz9eCDD2rFihXKzMzU1VdfrZdeekmdO3f26esYzpOsAACASXBLm3IoLCxU06ZNtX79emVmZmrcuHHq16+fcnJyfPo6LLcAAIA5eRyscnNztX//fmVnZ7vsS01N1cCBAxUVFaWoqCjddttt2rlzp08L9Qd16tTRxIkT1aRJEwUFBWnYsGEKDQ296NITFWEwyQoAANPwOFi99tpratGihXbs2OG0PSsrS927d9fnn3+uM2fO6MyZM/riiy/Uo0cPHTt2zOcFS1JOTo4mTZqkPn36KDo6WhaLRbNmzXJ7bH5+vp544gnFxcUpIiJCSUlJ+uqrr3xSx549e3Tq1CklJib65HxFmLwOAIA5eRysvv76azVp0sTlVjbTp0/X4cOH1b17d+3fv1/Hjx/XX/7yF504cUIvvfSSzwuWpIyMDE2ZMkU7duxQu3btyjx25MiRevHFF3XPPffolVdeUXBwsPr27av169dXqIbc3FwNHz5cycnJioqKqtC5SnLspSJXAQBgHh4Hq19++UXdunVz2f7pp5/KYrHoP//5j5o2baoGDRroX//6l1q2bKnly5f7tNgisbGxSk9P18GDB5WSklLqcZs3b9aHH36oqVOnKiUlRWPGjNGqVauUkJDgMum+a9euslgsbj+eeuopp2MLCgo0ZMgQJSYmauLEiT7/+rhVIAAA5uRxsDpx4oSaNGnitC03N1c//vijrrnmGjVr1sxpX69evbR//37fVFlCWFiYGjdufNHjFixYoODgYI0ZM8a+LTw8XKNHj9bGjRt16NAh+/b169fLMAy3H88884z9OJvN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cQFwj6FAAH5h7969Za68/re//a3Unp4nnnhCn3zyid566y1t375d3bp10+HDh/Xxxx+rX79+Wrx4sdOk85iYGH3wwQcaMmSI2rVrpz59+ujKK69Ufn6+UlNTtXbtWnXp0sWryfSOwsLCNHToUL399tuaOXOmEhIS1L17d4+e269fPzVt2lQvvPCCtm/frjZt2mjXrl1asmSJBg4cqAULFrg859FHH9WYMWPUuXNnDRkyRDabTUuXLlXHjh3dvsYbb7yhXbt26fHHH9d7772nzp07q379+jp06JC+++477dmzR+np6apdu3aF2gEIJAQrAH5h3759mjx5cqn7x40bV2qwqlevnr7++mslJyfrs88+03fffafWrVvrgw8+0P79+7V48WKXiee33nqrtm7dqpSUFK1YsUJfffWV6tSpo8svv1yjRo0q9b6F5XXvvffq7bffVkFBge6++25ZLBaPnle3bl2tWrVKf/3rX/X1119rzZo1at26tebOnatGjRq5DVZ/+tOfVFBQoJdfflnvvvuuYmNjNXLkSD311FMKDQ11OT46OlrffPONpk+fro8++khz586VzWZT48aN1a5dO02YMMGjqxcBFLMY7vp/AaCGGD58uObOnatffvlFV111VXWXA6CGY44VgBohPT3dZdvatWv14YcfqlWrVoQqAFWCoUAANULfvn0VERGh9u3bq06dOvrll1+0bNkyBQcH67XXXqvu8gAECIYCAdQIL7/8subOnat9+/bpzJkzql+/vm644QYlJyfbl1cAgMpGsAIAAPAR5lgBAAD4CMEKAADARwhWAAAAPkKwAgAA8BGCFQAAgI8QrAAAAHyEYAUAAOAjBCsAAAAfIVgBAAD4yP8Hbkg1g1GzoHsAAAAASUVORK5CYII=",
      "text/plain": [
       "<Figure size 640x480 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "def get_esd_plot(eigenvalues, weights):\n",
    "    density, grids = density_generate(eigenvalues, weights)\n",
    "    plt.semilogy(grids, density + 1.0e-7)\n",
    "    plt.ylabel('Density (Log Scale)', fontsize=14, labelpad=10)\n",
    "    plt.xlabel('Eigenvlaue', fontsize=14, labelpad=10)\n",
    "    plt.xticks(fontsize=12)\n",
    "    plt.yticks(fontsize=12)\n",
    "    plt.axis([np.min(eigenvalues) - 1, np.max(eigenvalues) + 1, None, None])\n",
    "    return plt.gca()\n",
    "\n",
    "def density_generate(eigenvalues,\n",
    "                     weights,\n",
    "                     num_bins=10000,\n",
    "                     sigma_squared=1e-5,\n",
    "                     overhead=0.01):\n",
    "\n",
    "    eigenvalues = np.array(eigenvalues)\n",
    "    weights = np.array(weights)\n",
    "\n",
    "    lambda_max = np.mean(np.max(eigenvalues, axis=1), axis=0) + overhead\n",
    "    lambda_min = np.mean(np.min(eigenvalues, axis=1), axis=0) - overhead\n",
    "\n",
    "    grids = np.linspace(lambda_min, lambda_max, num=num_bins)\n",
    "    sigma = sigma_squared * max(1, (lambda_max - lambda_min))\n",
    "\n",
    "    num_runs = eigenvalues.shape[0]\n",
    "    density_output = np.zeros((num_runs, num_bins))\n",
    "\n",
    "    for i in range(num_runs):\n",
    "        for j in range(num_bins):\n",
    "            x = grids[j]\n",
    "            tmp_result = gaussian(eigenvalues[i, :], x, sigma)\n",
    "            density_output[i, j] = np.sum(tmp_result * weights[i, :])\n",
    "    density = np.mean(density_output, axis=0)\n",
    "    normalization = np.sum(density) * (grids[1] - grids[0])\n",
    "    density = density / normalization\n",
    "    return density, grids\n",
    "\n",
    "\n",
    "def gaussian(x, x0, sigma_squared):\n",
    "    return np.exp(-(x0 - x)**2 /\n",
    "                  (2.0 * sigma_squared)) / np.sqrt(2 * np.pi * sigma_squared)\n",
    "\n",
    "criterion = torch.nn.CrossEntropyLoss()\n",
    "\n",
    "# get a batch of data for computing the Hessian\n",
    "batch_x, batch_y, *others = next(iter(data_loader))\n",
    "batch_x = batch_x.to(args.device)\n",
    "batch_y = batch_y.to(args.device)\n",
    "\n",
    "# create the hessian computation module\n",
    "hessian_comp = hessian(model_visual, criterion, data=(batch_x, batch_y), cuda=True)\n",
    "# Now let's compute the top 2 eigenavlues and eigenvectors of the Hessian\n",
    "top_eigenvalues, top_eigenvector = hessian_comp.eigenvalues(top_n=2, maxIter=1000)\n",
    "print(\"The top two eigenvalues of this model are: %.4f %.4f\"% (top_eigenvalues[-1],top_eigenvalues[-2]))\n",
    "\n",
    "density_eigen, density_weight = hessian_comp.density()\n",
    "\n",
    "   \n",
    "ax = get_esd_plot(density_eigen, density_weight)\n"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.8.13 (default, Oct 21 2022, 23:50:54) \n[GCC 11.2.0]"
  },
  "vscode": {
   "interpreter": {
    "hash": "6869619afde5ccaa692f7f4d174735a0f86b1f7ceee086952855511b0b6edec0"
   }
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
